﻿using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing.Imaging;
using System.Globalization;
using System.IO;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using PaintDotNet;
using PaintDotNet.IndirectUI;
using PaintDotNet.PropertySystem;

namespace PDNJPEGXRPlugin
{
    public class JpegXRFileType : PropertyBasedFileType
    {
        public JpegXRFileType()
            : base("JPEG XR", FileTypeFlags.SupportsLoading | FileTypeFlags.SupportsSaving, new string[] { ".wdp", ".hdp", ".jxr" })
        {               
        }

        public override ControlInfo OnCreateSaveConfigUI(PropertyCollection props)
        {
            ControlInfo configUI = PropertyBasedFileType.CreateDefaultSaveConfigUI(props);

            configUI.SetPropertyControlValue("Quality", ControlInfoPropertyNames.Description, "A value of 100 guarantees lossless compression.");

            configUI.SetPropertyControlValue("BitDepth", ControlInfoPropertyNames.DisplayName, "Bit depth");
            configUI.SetPropertyControlType("BitDepth", PropertyControlType.RadioButton);
            PropertyControlInfo bitDepthControl = configUI.FindControlForPropertyName("BitDepth");
            bitDepthControl.SetValueDisplayName(BitDepth.Bpp8, "8 bit (grayscale)");
            bitDepthControl.SetValueDisplayName(BitDepth.Bpp24, "24 bit");
            bitDepthControl.SetValueDisplayName(BitDepth.Bpp32, "32 bit (alpha)");   

            configUI.SetPropertyControlValue("WriteMetadata", ControlInfoPropertyNames.DisplayName, "Metadata");
            configUI.SetPropertyControlValue("WriteMetadata", ControlInfoPropertyNames.Description, "Write Exif metadata");

            return configUI;
        }

        protected override Document OnLoad(Stream input)
        {
            WmpBitmapDecoder decoder = new WmpBitmapDecoder(input, BitmapCreateOptions.None, BitmapCacheOption.None);

            if (decoder.Frames.Count == 0)
                throw new ApplicationException("File does not contain any image data.");

            BitmapFrame frame = decoder.Frames[0];

            Document document = new Document(frame.PixelWidth, frame.PixelHeight)
            {
                DpuUnit = MeasurementUnit.Inch,
                DpuX = frame.DpiX,
                DpuY = frame.DpiY
            };

            BitmapLayer layer = Layer.CreateBackgroundLayer(document.Width, document.Height);

            BitmapSource bitmapSource = frame;

            if (frame.Format != PixelFormats.Bgra32)
                bitmapSource = new FormatConvertedBitmap(frame, PixelFormats.Bgra32, null, 0);

            bitmapSource.CopyPixels(Int32Rect.Empty, layer.Surface.Scan0.Pointer, (int)layer.Surface.Scan0.Length, layer.Surface.Stride);

            document.Layers.Add(layer);

            BitmapMetadata metadata = frame.Metadata as BitmapMetadata;

            if (metadata != null)
            {
                ImportMetadata(metadata, document.Metadata, "/ifd");
                ImportMetadata(metadata, document.Metadata, "/ifd/exif");
            }

            return document;
        }

        private void ImportMetadata(BitmapMetadata source, Metadata destination, string query)
        {
            BitmapMetadata ifd = source.GetQuery(query) as BitmapMetadata;

            if (ifd == null)
                return;

            foreach (string item in ifd)
                if (item.StartsWith("/{ushort=", StringComparison.InvariantCultureIgnoreCase) && item.EndsWith("}", StringComparison.InvariantCultureIgnoreCase))
                {
                    ExifTagID id = (ExifTagID)Convert.ToUInt16(item.Substring(9, item.Length - 10));
                    object value = ifd.GetQuery(item);

                    Debug.Print(string.Format("Metadata import: {0}: ({1}) {2}", ((ushort)id).ToString("x4"), value.GetType().Name, value));

                    if (value is string)
                        destination.AddExifValues(new PropertyItem[] { Exif.CreateAscii(id, (string)value) });
                    else if (value is byte)
                        destination.AddExifValues(new PropertyItem[] { Exif.CreateByte(id, (byte)value) });
                    else if (value is ushort)
                        destination.AddExifValues(new PropertyItem[] { Exif.CreateShort(id, (ushort)value) });
                    else if (value is int)
                        destination.AddExifValues(new PropertyItem[] { Exif.CreateSLong(id, (int)value) });
                    else if (value is uint)
                        destination.AddExifValues(new PropertyItem[] { Exif.CreateLong(id, (uint)value) });
                    else if (value is long)
                        destination.AddExifValues(new PropertyItem[] { Exif.CreatePropertyItem(id, ExifTagType.SRational, BitConverter.GetBytes((long)value)) });
                    else if (value is ulong)
                        destination.AddExifValues(new PropertyItem[] { Exif.CreatePropertyItem(id, ExifTagType.Rational, BitConverter.GetBytes((ulong)value)) });    
                    else if (value is BitmapMetadataBlob)
                        destination.AddExifValues(new PropertyItem[] { Exif.CreatePropertyItem(id, ExifTagType.Undefined, ((BitmapMetadataBlob)value).GetBlobValue()) });                                  
                }
        }

        private string GetExifTagGroup(ushort exifTagId)
        {
            switch (exifTagId)
            {
                case 0x0100:
                case 0x0101:
                case 0x0102:
                case 0x0103:
                case 0x0106:
                case 0x010e:
                case 0x010f:
                case 0x0110:
                case 0x0112:
                case 0x0115:
                case 0x0116:
                case 0x011a:
                case 0x011b:
                case 0x011c:
                case 0x0128:
                case 0x012d:
                case 0x0131:
                case 0x0132:
                case 0x013b:
                case 0x013e:
                case 0x013f:
                case 0x0211:
                case 0x0212:
                case 0x0213:
                case 0x0214:
                case 0x8298:
                    return "/ifd";
                default:
                    return "/ifd/exif";
            }
        }

        private void ExportMetadata(Metadata source, BitmapMetadata destination)
        {
            const uint metadataPadding = 2048;

            destination.SetQuery("/ifd/PaddingSchema:padding", metadataPadding);
            destination.SetQuery("/ifd/exif", new BitmapMetadata("exif"));
            destination.SetQuery("/ifd/exif/PaddingSchema:padding", metadataPadding);
            destination.SetQuery("/ifd/xmp", new BitmapMetadata("xmp"));
            destination.SetQuery("/ifd/xmp/PaddingSchema:Padding", metadataPadding);

            foreach (string key in source.GetKeys(Metadata.ExifSectionName))
            {
                string valueString = source.GetValue(Metadata.ExifSectionName, key);

                if (!valueString.StartsWith("<exif", StringComparison.InvariantCultureIgnoreCase))
                    continue;

                int i = valueString.IndexOf("id=\"", StringComparison.InvariantCultureIgnoreCase);

                if (i == -1)
                    continue;

                int j = valueString.IndexOf('"', i + 4);

                if (j == -1)
                    continue;

                int intID;

                if (!int.TryParse(valueString.Substring(i + 4, j - i - 4), NumberStyles.Integer, CultureInfo.InvariantCulture, out intID))
                    continue;

                ushort id = (ushort)intID;

                PropertyItem[] exifValues = source.GetExifValues((ExifTagID)id);

                if (exifValues.Length == 0)
                    continue;

                PropertyItem exifValue = exifValues[0];

                object value;

                try
                {
                    switch ((ExifTagType)exifValue.Type)
                    {
                        case ExifTagType.Ascii:
                            value = Exif.DecodeAsciiValue(exifValue);
                            break;
                        case ExifTagType.Byte:
                            value = Exif.DecodeByteValue(exifValue);
                            break;
                        case ExifTagType.Long:
                            value = Exif.DecodeLongValue(exifValue);
                            break;
                        case ExifTagType.Rational:
                            value = BitConverter.ToUInt64(exifValue.Value, 0);
                            break;
                        case ExifTagType.Short:
                            value = Exif.DecodeShortValue(exifValue);
                            break;
                        case ExifTagType.SLong:
                            value = Exif.DecodeSLongValue(exifValue);
                            break;
                        case ExifTagType.SRational:
                            value = BitConverter.ToInt64(exifValue.Value, 0);
                            break;
                        case ExifTagType.Undefined:
                            value = new BitmapMetadataBlob(exifValue.Value);
                            break;
                        default:
                            continue;
                    }

                    destination.SetQuery(GetExifTagGroup(id) + "/{ushort=" + id + "}", value);
                    Debug.Print("Metadata export: " + GetExifTagGroup(id) + "/{ushort=" + id + "}: " + value);
                }
                catch (Exception e)
                {
                    Debug.Print("Metadata export: " + GetExifTagGroup(id) + "/{ushort=" + id + "}: FAILED, " + e.Message);
                }
            }
        }

        public enum BitDepth
        {
            Bpp8,
            Bpp24,
            Bpp32
        }

        public override PropertyCollection OnCreateSavePropertyCollection()
        {
            Property[] properties = new Property[]
            {
                new Int32Property("Quality", 90, 0, 100), 
                StaticListChoiceProperty.CreateForEnum<BitDepth>("BitDepth", BitDepth.Bpp24, false),
                new BooleanProperty("WriteMetadata", false),              
            };

            return new PropertyCollection(properties);
        }

        protected override void OnSaveT(Document input, Stream output, PropertyBasedSaveConfigToken token, Surface scratchSurface, ProgressEventHandler progressCallback)
        {
            using (RenderArgs args = new RenderArgs(scratchSurface))
                input.Render(args, true);

            double dpuX;
            double dpuY;

            switch (input.DpuUnit)
            {
                case MeasurementUnit.Centimeter:
                    dpuX = Document.DotsPerCmToDotsPerInch(input.DpuX);
                    dpuY = Document.DotsPerCmToDotsPerInch(input.DpuY);
                    break;
                case MeasurementUnit.Inch:
                    dpuX = input.DpuX;
                    dpuY = input.DpuY;
                    break;
                case MeasurementUnit.Pixel:
                    dpuX = Document.GetDefaultDpu(MeasurementUnit.Inch);
                    dpuY = Document.GetDefaultDpu(MeasurementUnit.Inch);
                    break;
                default:
                    throw new InvalidEnumArgumentException();
            }

            BitmapSource bitmapSource = BitmapSource.Create(scratchSurface.Width, scratchSurface.Height, dpuX, dpuY, PixelFormats.Bgra32, null, scratchSurface.Scan0.Pointer, (int)scratchSurface.Scan0.Length, scratchSurface.Stride);

            BitmapSource source = bitmapSource;            

            System.Windows.Media.PixelFormat targetPixelFormat;
            
            switch ((BitDepth)token.GetProperty<StaticListChoiceProperty>("BitDepth").Value)
            {
                case BitDepth.Bpp8:
                    targetPixelFormat = PixelFormats.Gray8;
                    break;
                case BitDepth.Bpp24:
                    targetPixelFormat = PixelFormats.Bgr24;
                    break;
                case BitDepth.Bpp32:
                    targetPixelFormat = PixelFormats.Bgra32;
                    break;
                default:
                    throw new InvalidEnumArgumentException();
            }     

            if (bitmapSource.Format != targetPixelFormat)
                source = new FormatConvertedBitmap(bitmapSource, targetPixelFormat, null, 0.0);

            BitmapMetadata metadata = null;

            if (token.GetProperty<BooleanProperty>("WriteMetadata").Value)
            {
                metadata = new BitmapMetadata("wmphoto");
                ExportMetadata(input.Metadata, metadata);
            }

            BitmapFrame frame = BitmapFrame.Create(source, null, metadata, null);

            WmpBitmapEncoder encoder = new WmpBitmapEncoder();

            encoder.Frames.Add(frame);
            
            encoder.ImageQualityLevel = token.GetProperty<Int32Property>("Quality").Value / 100f;

            encoder.Save(output);
        }
    }
}
